package com.stottlerhenke.simbionic.editor;

import java.net.URL;
import java.util.*;
import java.io.File;

import com.stottlerhenke.simbionic.engine.file.SB_TextFileReader;
import com.stottlerhenke.simbionic.common.SB_Logger;
import com.stottlerhenke.simbionic.common.SB_Tokenizer;
import com.stottlerhenke.simbionic.common.classes.SB_ClassDescription;
import com.stottlerhenke.simbionic.common.classes.SB_ClassMap;


/**
 * This class imports java class specication files. See the SimBionic
 * documentation for more details on the format of this file.
 * 
 */
public class SB_ClassSpecificationFileImport
{
  
  /**
   * Return class when parsing comments. Comment will contain the comment, blank if none.
   * nextToken will contain the next token, which should be a header token.
   */
  class ClassSpecificationComment
  {
    public String comment = new String();
    public String nextToken = new String();
  }
  
  /**
   * This class wraps the SB_Tokenizer with functionality to push back tokens.
   */
  public class SB_PushBackTokenizer
  {
    public SB_PushBackTokenizer(SB_Tokenizer t)
    {
      _t = t;
      _previousIndex = new ArrayList();
    }
    
    public String getNextToken()
    {
      //Only keep a number of elements
      if( _previousIndex.size() > PUSHBACK_MEMORY_SIZE )
        _previousIndex.remove(0);
      
      _previousIndex.add(new Integer(_t.getCurrentIndexPosition()));
      return _t.getNextToken();
    }
    
    public boolean hasMoreTokens()
    {
      return _t.hasMoreTokens();
    }
    
    public String setDelimiters(String s)
    {
      return _t.setDelimiters(s);
    }
    
    public void pushBackToken()
    {
      int count = _previousIndex.size();
      
      if( count >= 1 )
      {
        Integer previous = (Integer) _previousIndex.get(count - 1);
        _t.setCurrentIndexPosition( previous.intValue());
        _previousIndex.remove(count - 1);
      }
    }
    
    SB_Tokenizer _t;
    ArrayList _previousIndex; //Array of Integer objects
    final int PUSHBACK_MEMORY_SIZE = 100;
  }
  
  /**
   * Create a new import class
   */
  public SB_ClassSpecificationFileImport(SB_ClassMap map, SB_Logger logger)
  {
    classMap = map;
    log = logger;
  }
  
  /**
   * Initialize spec importing
   * 
   * @param url
   * @throws Exception
   */
  public void ImportClassSpec(URL url) throws Exception
  {    
    //Open a text file reader
    SB_TextFileReader reader = new SB_TextFileReader(url);
    reader.prepare();
    
    SB_PushBackTokenizer tokenizer = new SB_PushBackTokenizer(reader.getTokenizer());
    
    
    //While not end of the stream, read a class from the stream
    while(tokenizer.hasMoreTokens())
    {
      ImportClass(tokenizer);
    }
    
    int temp = 1;
  }
  
  /**
   * Import a single class into the class map
   * @param t
   * @throws Exception
   */
  public void ImportClass(SB_PushBackTokenizer t) throws Exception
  {
    ClassSpecificationComment comment = ImportComment(t);
    String header = comment.nextToken;
    
    if( header.compareTo(beginClass) != 0)
      throw new Exception("Expecting 'class' received " + header);
    
    String name = t.getNextToken();
    SB_ClassDescription description = classMap.getClassDescription(name);
    
    log.log("Starting loading class: " + name, SB_Logger.MILESTONE);
    
    //Read the alias if there
    String nextToken = t.getNextToken();
    String alias = null;
    if( nextToken.compareTo(beginAlias) == 0)
    {
      alias = t.getNextToken();
    }
    else
      t.pushBackToken();
    
    if( description == null )
    {
      classMap.addJavaClass(name, name);
      description = classMap.getClassDescription(name);
    }
    
    //Set the alias even if the class is already entered
    //NOTE: Use addAdditionalSBClassName not addJavaClass
    if( alias != null )
    {
      if( classMap.isDefinedSBClass(alias))
      {
        log.log("Alias (" + alias + ") is already in use", SB_Logger.ERROR);
      }
      
      classMap.addAdditionalSBClassName(alias, name);
      log.log("\tAlias is: " + alias);
    }
    
    log.log(comment.comment);
    
    commentMap.put(name, comment.comment);
    
    ImportMemberList(t, name, description);
    
    log.log("Finished loading class: " + name, SB_Logger.MILESTONE);
  }
  
  /**
   * Import a comment. Comment will be "" if no comment given. This also reads
   * the next token, returned in nextToken.
   * 
   * @param t
   * @return
   * @throws Exception
   */
  public ClassSpecificationComment ImportComment(SB_PushBackTokenizer t) throws Exception
  {
    ClassSpecificationComment comment = new ClassSpecificationComment();
    comment.nextToken = t.getNextToken();
    
    if( comment.nextToken.indexOf("/*") == 0)
    {
      boolean bSaveComment = false;
      if( comment.nextToken.indexOf("/**") == 0)
        bSaveComment = true;
      
      StringBuffer buf = new StringBuffer();
      
      if( !bSaveComment)
        comment.nextToken = comment.nextToken.substring(2);
      else
        comment.nextToken = comment.nextToken.substring(3);
      
      int pos = comment.nextToken.indexOf("*/");
      if( pos >= 0) //Check to see if we have already found the end comment
        buf.append(comment.nextToken.substring(0, pos));
      else //If not, read up until the end comment
      {
        buf.append(comment.nextToken);
        String oldDelims = t.setDelimiters("/");
	      //If the last item is * then we have found * /
	      do
	      {
	        comment.nextToken = t.getNextToken();
	        buf.append(comment.nextToken);
	      }
	      while( comment.nextToken.indexOf("*") != comment.nextToken.length() - 1 );
	      
	      t.setDelimiters(oldDelims); //Reset delimeters
	      t.getNextToken(); //Cleanup the end of line
	      buf.deleteCharAt(buf.length() - 1); //delete the '*'
      }
     
      comment.nextToken = t.getNextToken();
      if(bSaveComment)
        comment.comment = buf.toString();
      else
        comment.comment = new String();
      
      return comment;
    }
    else
      return comment;
  }
  
  /**
   * Read in all of the class memeber and methods, ending when endClass is reached
   * 
   * @param t
   * @param description
   * @throws Exception
   */
  public void ImportMemberList(SB_PushBackTokenizer t, String className, SB_ClassDescription description)
  	throws Exception
  {
    while(t.hasMoreTokens())
    {
	    ClassSpecificationComment comment = ImportComment(t);
	    String header = comment.nextToken;
	    
	    if( header.compareTo(beginVar) == 0)
	      ImportVarSpec(t, description, className, comment.comment);
	    else
	    if( header.compareTo(beginMethod) == 0) 
	      ImportMethodSpec(t, description, className, comment.comment);
	    else
	    if( header.compareTo(endClass) == 0)
	    {
	      break;
	    }
	    //else
	    //  throw new Exception("Expecting 'var' or 'method' or 'endclass'\n(received '" + header + "')");
    }
  }
  
  /**
   * Import a single member description
   * 
   * @param t
   * @param description
   * @param comment
   * @throws Exception
   */
  public void ImportVarSpec(SB_PushBackTokenizer t, SB_ClassDescription description, String className, String comment)
  	throws Exception
  {
    Class c = ImportType(t);
    String name = t.getNextToken();
    description.addMemberDescription(name, c);
    commentMap.put( className + ":" + name, comment);
    
    log.log("\tLoaded member: " + name, SB_Logger.MILESTONE);
  }
  
  /**
   * Import the type information
   * 
   * @param t
   * @return a Class object of the correct type
   * @throws Exception
   */
  public Class ImportType(SB_PushBackTokenizer t) throws Exception
  {
    String header = t.getNextToken();
    
    if( header.compareTo(beginStatic) == 0) //TODO: store static here
      header = t.getNextToken();
    
    Class c = convertToClass(header);
    
    if( c != null && !isPrimitive(header) )
    {  
      if(!classMap.isDefinedJavaClass(c))
        classMap.addJavaClass(header, header);
    }
    
    return c;
  }
  
  /**
   * 
   * @param s
   * @return true if s is a primitive type, false otherwise
   */
  public boolean isPrimitive(String s)
  {
    if(s.compareTo("void") == 0)
      return true;
    else
    if(s.compareTo("byte") == 0)
      return true;
    else
    if(s.compareTo("short") == 0)
      return true;
    else
    if(s.compareTo("int") == 0)
      return true;
    else
    if(s.compareTo("long") == 0)
      return true;
    else
    if(s.compareTo("float") == 0)
      return true;
    else
    if(s.compareTo("double") == 0)
      return true;
    else
    if(s.compareTo("char") == 0)
      return true;
    else
    if(s.compareTo("boolean") == 0)
      return true;
    
    return false;
  }
  /**
   * Convert string to java class
   * 
   * @param s
   * @return
   */
  public Class convertToClass(String s) throws ClassNotFoundException
  {
    if(s.compareTo("void") == 0)
      return null;
    else
    if(s.compareTo("byte") == 0)
      return Byte.TYPE;
    else
    if(s.compareTo("short") == 0)
      return Short.TYPE;
    else
    if(s.compareTo("int") == 0)
      return Integer.TYPE;
    else
    if(s.compareTo("long") == 0)
      return Long.TYPE;
    else
    if(s.compareTo("float") == 0)
      return Float.TYPE;
    else
    if(s.compareTo("double") == 0)
      return Double.TYPE;
    else
    if(s.compareTo("char") == 0)
      return Character.TYPE;
    else
    if(s.compareTo("boolean") == 0)
      return Boolean.TYPE;
    
    return ClassLoader.getSystemClassLoader().loadClass(s);
  }
  
  /**
   * Import a single method, includig its parameters
   * 
   * @param t
   * @param description
   * @param comment
   * @throws Exception
   */
  public void ImportMethodSpec(SB_PushBackTokenizer t, SB_ClassDescription description, String className, String comment)
  	throws Exception
  {
    ArrayList argClasses = new ArrayList();
    ArrayList argNames = new ArrayList();
    
    Class c = ImportType(t);
    
    //Read in just the arg line
    String oldDelims = t.setDelimiters("(");
    String name = t.getNextToken();
    
    log.log("\tLoading method: " + name, SB_Logger.MILESTONE);

    
    t.setDelimiters(")");
    String args = t.getNextToken();
    t.setDelimiters(oldDelims);
    
    //Create a new tokenizer on the arg line and read in arguments
    SB_Tokenizer tempSBTokenizer = new SB_Tokenizer();
    tempSBTokenizer.setString(args);
    SB_PushBackTokenizer argT = new SB_PushBackTokenizer( tempSBTokenizer );
    argT.setDelimiters(" ,\r\n");
    while(argT.hasMoreTokens())
    {
      Class tempClass = ImportType(argT);
      String tempName = argT.getNextToken();
      argClasses.add(tempClass);
      argNames.add(tempName);
    }
    
    String signature="";
    for (int si=0;si<argClasses.size();si++){
    	if (si>0) signature = signature + ", ";
    	signature = signature + ((Class)argClasses.get(si)).getName();
    }
    signature = "(" + signature +")";
    
    description.addMethodDescription(name, c, argClasses, argNames);
    commentMap.put( className + ":" + name + signature, comment); 
    
    StringBuffer argBuffer = new StringBuffer();
    for( int x = 0; x < argClasses.size(); x++)
    {
      if( argClasses.get(x) != null)
      {
        argBuffer.append(argClasses.get(x).toString() + " ");
        
        if( argNames.size() > x && argNames.get(x) != null)
        {
          argBuffer.append(argNames.get(x).toString() + " ");
        }
      }
      else
      {
        log.log("Invalid parameter type", SB_Logger.ERROR);
        argBuffer.append("null");
      }
    }
    
    log.log("\tLoaded method: " + name + " (" + argBuffer.toString() + ")" , SB_Logger.MILESTONE);
  }
  
  public HashMap getCommentMap(){
  	return commentMap;
  }

  private final String beginStatic = new String("static");
  private final String beginVar = new String("var");
  private final String beginMethod = new String("method");
  private final String beginClass = new String("class");
  private final String endClass = new String("endclass");
  private final String beginAlias = "alias";
  
  private SB_ClassMap classMap;
  private HashMap commentMap = new HashMap(); //key is Class or Class:Method or Class:Var; value is comment string
  private SB_Logger log;
  
  /**
   * Temporary testing main
   */
  public static void main(String[] args)
  {
    SB_ClassMap map = new SB_ClassMap();
    SB_Logger log = new SB_Logger();
    log.register(System.out, SB_Logger.ALL);
    SB_ClassSpecificationFileImport i = 
    	new SB_ClassSpecificationFileImport(map, log);
    
    File file = new File("c:\\base_classes.txt");
    
    try
    {
      i.ImportClassSpec( file.toURI().toURL() );
    }
    catch(Exception ex)
    {
      System.out.println(ex.toString());
    }
  }
  
}
